/*
 * IncredulousPanel.java
 *
 * Created on 30. november 2007, 23:23
 */
package incredulousmachine;

import static java.awt.RenderingHints.*;
import static java.awt.event.KeyEvent.*;
import static java.awt.event.MouseEvent.*;
import static java.awt.Color.*;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RadialGradientPaint;
import java.awt.Rectangle;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JPanel;
import javazoom.jlgui.basicplayer.BasicPlayer;
import javazoom.jlgui.basicplayer.BasicPlayerException;
import net.phys2d.math.*;
import net.phys2d.raw.*;
import net.phys2d.raw.shapes.*;
import net.phys2d.raw.strategies.QuadSpaceStrategy;

/**
 *
 * @author  tomase
 */
public class IncredulousPanel extends JPanel {
    //Properties - loaded from game.properties
    Properties gameProperties;
    //World constants - these defaults can be changed on init
    private float globalDamping = 0.95f;
    private float gravityVal = 0.98f;
    private Vector2f gravity = new Vector2f(0f, 0.98f);
    private int worldIterationsPerFrame = 10;
    private int maxNumBodies = 100;
    private int maxNumDivisions = 20;
    private float globalRestHitTolerance = 0.1f;
    private float globalRestVelocityTolerance = 0.01f;
    private float globalRestRotationalTolerance = 0.01f;
    private BroadCollisionStrategy collisionStrategy = new QuadSpaceStrategy(100, 20);
    private World gameWorld = new World(gravity, worldIterationsPerFrame, collisionStrategy);
    private String musicDirectory;
    private float musicGain = 0.8f;
    private HashMap<Body, Color> bodyColours = new HashMap<Body, Color>();
    private Color[] acceptableColours = {BLACK,
        BLUE,
        RED,
        GREEN,
        ORANGE,
        DARK_GRAY
    };
    private Random random = new Random();
    private boolean reset = false;
    private Animator animator;
    private List<Point> drawn = new LinkedList<Point>();
    private BasicPlayer musicPlayer;

    /** Creates new form IncredulousPanel */
    public IncredulousPanel() {
        gameProperties = loadProperties();
        readProperties(gameProperties);

        initWorld(gameWorld);
        initComponents();
        setFocusable(true);
        requestFocusInWindow();

        BodyBuilder bb = new BodyBuilder();
        addMouseListener(bb);
        addMouseMotionListener(bb);

        addKeyListener(new KeyMaster());

        animator = new Animator();
        new Thread(animator).start();

        playMusicFromDirectory(musicDirectory);
    }

    private void readProperties(Properties props) {
        globalDamping = Float.valueOf(props.getProperty("world_damping", Float.toString(globalDamping)));
        gravityVal = Float.valueOf(props.getProperty("world_gravity", Float.toString(gravityVal)));
        worldIterationsPerFrame = Integer.valueOf(props.getProperty("world_iteration_resolution", Integer.toString(worldIterationsPerFrame)));
        maxNumBodies = Integer.valueOf(props.getProperty("world_allowed_bodies", Integer.toString(maxNumBodies)));
        maxNumDivisions = Integer.valueOf(props.getProperty("world_collision_subdivisions", Integer.toString(maxNumDivisions)));
        globalRestHitTolerance = Float.valueOf(props.getProperty("world_rest_hit_tolerance", Float.toString(globalRestHitTolerance)));
        globalRestVelocityTolerance = Float.valueOf(props.getProperty("world_rest_velocity_tolerance", Float.toString(globalRestVelocityTolerance)));
        globalRestRotationalTolerance = Float.valueOf(props.getProperty("world_rest_rotation_tolerance", Float.toString(globalRestRotationalTolerance)));
        
        musicDirectory = props.getProperty("music_directory");
        musicGain = Float.valueOf(props.getProperty("music_gain", Float.toString(musicGain)));
        
        gravity = new Vector2f(0.0f, gravityVal);
        collisionStrategy = new QuadSpaceStrategy(maxNumBodies, maxNumDivisions);
        gameWorld = new World(gravity, worldIterationsPerFrame, collisionStrategy);
    }

    private Properties loadProperties() {
        Properties props = new Properties();
        try {

            File propsFile = new File("game.properties");
            props.load(new FileReader(propsFile));

            return props;
        } catch (FileNotFoundException ex) {
            Logger.getLogger(IncredulousPanel.class.getName()).log(
                    Level.WARNING,
                    "Game properties file not found. Game will proceed with defaults.",
                    ex);
        } catch (IOException ex) {
            Logger.getLogger(IncredulousPanel.class.getName()).log(
                    Level.WARNING,
                    "IO exception reading properties file. Game will proceed with defaults.",
                    ex);
        }
        return props;
    }

    private void playMusicFromDirectory(String folder) {
        File musicFolder = new File(folder);
        List<File> songs = new LinkedList<File>();

        if (!musicFolder.isDirectory()) {
            return; //no music for you
        }

        FilenameFilter onlySongs = new FilenameFilter() {

            public boolean accept(File dir, String name) {
                return name.endsWith(".mp3");
            }
        };

        String[] songFilenames = musicFolder.list(onlySongs);
        for (String songName : songFilenames) {
            songs.add(new File(musicFolder.getPath() + File.separator + songName));
        }

        musicPlayer = new BasicPlayer();

        try {

            File currentSong = songs.get(random.nextInt(songs.size()));
            System.out.println("Playing " + currentSong.getAbsolutePath());

            musicPlayer.open(currentSong);
            float minGain = musicPlayer.getMinimumGain();
            float maxGain = musicPlayer.getMaximumGain();
            float gain = minGain + (maxGain - minGain) * musicGain;
            musicPlayer.setGain(gain);
            musicPlayer.play();

        } catch (BasicPlayerException ex) {
            Logger.getLogger(IncredulousPanel.class.getName()).log(Level.SEVERE, null, ex);
        }

    }

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        setBackground(new java.awt.Color(255, 255, 255));
        setMinimumSize(new java.awt.Dimension(600, 400));
        setPreferredSize(new java.awt.Dimension(600, 400));

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
        this.setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGap(0, 600, Short.MAX_VALUE)
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGap(0, 400, Short.MAX_VALUE)
        );
    }// </editor-fold>//GEN-END:initComponents
    // Variables declaration - do not modify//GEN-BEGIN:variables
    // End of variables declaration//GEN-END:variables
    @Override
    public void paintComponent(Graphics g) {
        //Clear screen
        Color old = g.getColor();
        g.setColor(WHITE);
        g.fillRect(0, 0, getWidth(), getHeight());
        g.setColor(old);
        Graphics2D g2d = (Graphics2D) g;
        g2d.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);

        //draw bodies
        Body paintMe;

        for (int i = 0; i <
                gameWorld.getBodies().size(); i++) {
            paintMe = gameWorld.getBodies().get(i);

            drawBody(g2d, paintMe);
        }

//draw input from user
        if (drawn.size() > 2) {
            g2d.setColor(BLACK);
            Point current,
                    next;
            for (int i = 0; i <
                    drawn.size() - 1; i++) {
                current = drawn.get(i);
                next =
                        drawn.get(i + 1);
                g2d.drawLine(current.x, current.y, next.x, next.y);
            }

        }

        //draw collisions
        ArbiterList arbiters = gameWorld.getArbiters();
        for (int i = 0; i <
                arbiters.size(); i++) {
            Arbiter a = arbiters.get(i);

            Contact[] contacts = a.getContacts();

            for (Contact c : contacts) {
                drawContact(g2d, c);
            }

        }


        g2d.dispose();
    }

    private void initWorld(World w) {
        w.setDamping(globalDamping);
        w.enableRestingBodyDetection(globalRestHitTolerance, globalRestRotationalTolerance, globalRestVelocityTolerance);
        Body body1 = new StaticBody("Ground1", new Box(610.0f, 10.0f));
        body1.setPosition(300f, 400f);
        body1.setFriction(20f);
        body1.setRestitution(0.0f);
        addBody(body1);
    }

    /**
     * Removes all moveable bodies on the world outside the screen.
     * @param w The world.
     */
    private void pruneWorld(World w) {
        int padding = 80;
        Body candidate;

        ROVector2f pos;

        for (int i = 0; i <
                w.getBodies().size(); i++) {
            candidate = w.getBodies().get(i);
            pos =
                    candidate.getPosition();

            if (candidate.isMoveable() &&
                    (pos.getX() < -padding || pos.getX() > getWidth() + padding ||
                    pos.getY() < -padding || pos.getY() > getHeight() + padding)) {
                w.remove(candidate);
                System.out.println("Removed: " + candidate);
            }

        }
    }

    private Color getRandomColour() {
        return acceptableColours[random.nextInt(acceptableColours.length)];
    }

    protected void addBody(Body b) {
        bodyColours.put(b, getRandomColour());
        gameWorld.add(b);
    }

    private Color getColour(Body b) {
        return bodyColours.get(b);
    }

    /**
     * Draw a body 
     * 
     * @param g The graphics contact on which to draw
     * @param body The body to be drawn
     */
    protected void drawBody(Graphics2D g, Body body) {
        g.setColor(getColour(body));

        if (body.getShape() instanceof Box) {
            drawBoxBody(g, body, (Box) body.getShape());
        }

        if (body.getShape() instanceof Circle) {
            drawCircleBody(g, body, (Circle) body.getShape());
        }

        if (body.getShape() instanceof Line) {
            drawLineBody(g, body, (Line) body.getShape());
        }

        if (body.getShape() instanceof Polygon) {
            drawPolygonBody(g, body, (Polygon) body.getShape());
        }

    }

    /**
     * Draw a polygon into the demo
     * 
     * @param g The graphics to draw the poly onto
     * @param body The body describing the poly's position
     * @param poly The poly to be drawn
     */
    protected void drawPolygonBody(Graphics2D g, Body body, Polygon poly) {
        Graphics2D g2d = (Graphics2D) g.create();

//        //get the vertices
//        ROVector2f[] verts = poly.getVertices(body.getPosition(), body.getRotation());
//        for (int i = 0,  j = verts.length - 1; i < verts.length; j = i, i++) {
//            g2d.drawLine(
//                    (int) (0.5f + verts[i].getX()),
//                    (int) (0.5f + verts[i].getY()),
//                    (int) (0.5f + verts[j].getX()),
//                    (int) (0.5f + verts[j].getY()));
//        }

        ROVector2f[] verts = poly.getVertices();

        int[] xCoords = new int[verts.length];
        int[] yCoords = new int[verts.length];
        for (int i = 0; i <
                verts.length; i++) {
            xCoords[i] = (int) verts[i].getX();
            yCoords[i] = (int) verts[i].getY();
        }

//Information about the body in the current context
        ROVector2f velocity = body.getVelocity();
        ROVector2f centroid = poly.getCentroid();

        Point centre = new Point((int) centroid.getX(), (int) centroid.getY());
        Point velocityTarget = new Point((int) velocity.getX(), (int) velocity.getY());
        velocityTarget.translate(centre.x, centre.y);

        ROVector2f positionVector = body.getPosition();
        float rotation = body.getRotation();
        Point position = new Point((int) positionVector.getX(), (int) positionVector.getY());

        g2d.rotate(rotation, position.x, position.y);
        g2d.translate(position.x, position.y);

        //draw the polygon
        g2d.fillPolygon(xCoords, yCoords, verts.length);
        g2d.setColor(BLACK);
        g2d.drawPolygon(xCoords, yCoords, verts.length);

        //draw the force
        g2d.setColor(MAGENTA);
        g2d.drawLine(centre.x, centre.y, velocityTarget.x, velocityTarget.y);
        g2d.drawOval(velocityTarget.x, velocityTarget.y, 2, 2);

        //draw the Centroid 
        g2d.setColor(GREEN);
        g2d.fillOval((int) centroid.getX(), (int) centroid.getY(), 5, 5);
        g2d.setColor(GRAY);
        g2d.drawOval((int) centroid.getX(), (int) centroid.getY(), 5, 5);

        g2d.dispose();
    }

    /**
     * Draw a line into the demo
     * 
     * @param g The graphics to draw the line onto
     * @param body The body describing the line's position
     * @param line The line to be drawn
     */
    protected void drawLineBody(Graphics2D g, Body body, Line line) {
//
//		float x = body.getPosition().getX();
//		float y = body.getPosition().getY();
//		float dx = line.getDX();
//		float dy = line.getDY();
//		
//		g.drawLine((int) x,(int) y,(int) (x+dx),(int) (y+dy));
        Vector2f[] verts = line.getVertices(body.getPosition(), body.getRotation());
        g.drawLine(
                (int) verts[0].getX(),
                (int) verts[0].getY(),
                (int) verts[1].getX(),
                (int) verts[1].getY());
    }

    /**
     * Draw a circle in the world
     * 
     * @param g The graphics contact on which to draw
     * @param body The body to be drawn
     * @param circle The shape to be drawn
     */
    protected void drawCircleBody(Graphics2D g, Body body, Circle circle) {
        Graphics2D g2d = (Graphics2D) g.create();

        float x = body.getPosition().getX();
        float y = body.getPosition().getY();
        float r = circle.getRadius();

        Point2D centre = new Point2D.Float(x, y);

        Color a = g.getColor();
        Color b = a.darker();

        RadialGradientPaint paint =
                new RadialGradientPaint(centre, r,
                new float[]{0.0f, 1.0f},
                new Color[]{a, b});

        g2d.setPaint(paint);
        g2d.fillOval((int) (x - r), (int) (y - r), (int) (r * 2), (int) (r * 2));

        g2d.dispose();
    }

    /**
     * Draw a box in the world
     * 
     * @param g The graphics contact on which to draw
     * @param body The body to be drawn
     * @param box The shape to be drawn
     */
    protected void drawBoxBody(Graphics2D g, Body body, Box box) {
        ROVector2f centre = body.getPosition();
        float rotationInRad = body.getRotation();

        Vector2f[] nonRotatedPoints = box.getPoints(centre, 0f);
        int x = (int) nonRotatedPoints[0].x;
        int y = (int) nonRotatedPoints[0].y;
        int width = (int) (nonRotatedPoints[1].x - nonRotatedPoints[0].x);
        int height = (int) (nonRotatedPoints[2].y - nonRotatedPoints[0].y);

        Graphics2D g2d = (Graphics2D) g.create();
        g2d.rotate(rotationInRad, centre.getX(), centre.getY());
        g2d.fillRect(x, y, width, height);
        g2d.setColor(BLACK);
        g2d.drawRect(x, y, width, height);
        g2d.dispose();

//        Vector2f[] pts = box.getPoints(body.getPosition(), body.getRotation());

//        Vector2f v1 = pts[0];
//        Vector2f v2 = pts[1];
//        Vector2f v3 = pts[2];

//        int x = (int) v1.x;
//        int y = (int) v1.y;
//        int width = (int) (v2.x - v1.x);
//        int height = (int) (v3.y - v1.y);
//
//        g.fillRect(x, y, width, height);
//        Color retain = g.getColor();
//        g.setColor(Color.BLACK);
//        g.drawRect(x, y, width, height);
//        g.setColor(retain);

//
//        g.drawLine((int) v1.x, (int) v1.y, (int) v2.x, (int) v2.y);
//        g.drawLine((int) v2.x, (int) v2.y, (int) v3.x, (int) v3.y);
//        g.drawLine((int) v3.x, (int) v3.y, (int) v4.x, (int) v4.y);
//        g.drawLine((int) v4.x, (int) v4.y, (int) v1.x, (int) v1.y);
    }

    /**
     * Draw a joint 
     * 
     * @param g The graphics contact on which to draw
     * @param j The joint to be drawn
     */
    public void drawJoint(Graphics2D g, Joint j) {
        if (j instanceof FixedJoint) {
            FixedJoint joint = (FixedJoint) j;

            g.setColor(Color.red);
            float x1 = joint.getBody1().getPosition().getX();
            float x2 = joint.getBody2().getPosition().getX();
            float y1 = joint.getBody1().getPosition().getY();
            float y2 = joint.getBody2().getPosition().getY();

            g.drawLine((int) x1, (int) y1, (int) x2, (int) y2);
        }

        if (j instanceof SlideJoint) {
            SlideJoint joint = (SlideJoint) j;

            Body b1 = joint.getBody1();
            Body b2 = joint.getBody2();

            Matrix2f R1 = new Matrix2f(b1.getRotation());
            Matrix2f R2 = new Matrix2f(b2.getRotation());

            ROVector2f x1 = b1.getPosition();
            Vector2f p1 = MathUtil.mul(R1, joint.getAnchor1());
            p1.add(x1);

            ROVector2f x2 = b2.getPosition();
            Vector2f p2 = MathUtil.mul(R2, joint.getAnchor2());
            p2.add(x2);

            Vector2f im = new Vector2f(p2);
            im.sub(p1);
            im.normalise();



            g.setColor(Color.red);
            g.drawLine((int) p1.x, (int) p1.y, (int) (p1.x + im.x * joint.getMinDistance()), (int) (p1.y + im.y * joint.getMinDistance()));
            g.setColor(Color.blue);
            g.drawLine((int) (p1.x + im.x * joint.getMinDistance()), (int) (p1.y + im.y * joint.getMinDistance()), (int) (p1.x + im.x * joint.getMaxDistance()), (int) (p1.y + im.y * joint.getMaxDistance()));
        }

        if (j instanceof AngleJoint) {
            AngleJoint angleJoint = (AngleJoint) j;
            Body b1 = angleJoint.getBody1();
            Body b2 = angleJoint.getBody2();
            float RA = j.getBody1().getRotation() + angleJoint.getRotateA();
            float RB = j.getBody1().getRotation() + angleJoint.getRotateB();

            Vector2f VA = new Vector2f((float) Math.cos(RA), (float) Math.sin(RA));
            Vector2f VB = new Vector2f((float) Math.cos(RB), (float) Math.sin(RB));

            Matrix2f R1 = new Matrix2f(b1.getRotation());
            Matrix2f R2 = new Matrix2f(b2.getRotation());

            ROVector2f x1 = b1.getPosition();
            Vector2f p1 = MathUtil.mul(R1, angleJoint.getAnchor1());
            p1.add(x1);

            ROVector2f x2 = b2.getPosition();
            Vector2f p2 = MathUtil.mul(R2, angleJoint.getAnchor2());
            p2.add(x2);

            g.setColor(Color.red);
            g.drawLine((int) p1.x, (int) p1.y, (int) (p1.x + VA.x * 20), (int) (p1.y + VA.y * 20));
            g.drawLine((int) p1.x, (int) p1.y, (int) (p1.x + VB.x * 20), (int) (p1.y + VB.y * 20));
        }

        if (j instanceof BasicJoint) {
            BasicJoint joint = (BasicJoint) j;

            Body b1 = joint.getBody1();
            Body b2 = joint.getBody2();

            Matrix2f R1 = new Matrix2f(b1.getRotation());
            Matrix2f R2 = new Matrix2f(b2.getRotation());

            ROVector2f x1 = b1.getPosition();
            Vector2f p1 = MathUtil.mul(R1, joint.getLocalAnchor1());
            p1.add(x1);

            ROVector2f x2 = b2.getPosition();
            Vector2f p2 = MathUtil.mul(R2, joint.getLocalAnchor2());
            p2.add(x2);

            g.setColor(Color.red);
            g.drawLine((int) x1.getX(), (int) x1.getY(), (int) p1.x, (int) p1.y);
            g.drawLine((int) p1.x, (int) p1.y, (int) x2.getX(), (int) x2.getY());
            g.drawLine((int) x2.getX(), (int) x2.getY(), (int) p2.x, (int) p2.y);
            g.drawLine((int) p2.x, (int) p2.y, (int) x1.getX(), (int) x1.getY());
        }

        if (j instanceof DistanceJoint) {
            DistanceJoint joint = (DistanceJoint) j;

            Body b1 = joint.getBody1();
            Body b2 = joint.getBody2();

            Matrix2f R1 = new Matrix2f(b1.getRotation());
            Matrix2f R2 = new Matrix2f(b2.getRotation());

            ROVector2f x1 = b1.getPosition();
            Vector2f p1 = MathUtil.mul(R1, joint.getAnchor1());
            p1.add(x1);

            ROVector2f x2 = b2.getPosition();
            Vector2f p2 = MathUtil.mul(R2, joint.getAnchor2());
            p2.add(x2);

            g.setColor(Color.red);
            g.drawLine((int) p1.getX(), (int) p1.getY(), (int) p2.x, (int) p2.y);
        }

        if (j instanceof SpringJoint) {
            SpringJoint joint = (SpringJoint) j;

            Body b1 = joint.getBody1();
            Body b2 = joint.getBody2();

            Matrix2f R1 = new Matrix2f(b1.getRotation());
            Matrix2f R2 = new Matrix2f(b2.getRotation());

            ROVector2f x1 = b1.getPosition();
            Vector2f p1 = MathUtil.mul(R1, joint.getLocalAnchor1());
            p1.add(x1);

            ROVector2f x2 = b2.getPosition();
            Vector2f p2 = MathUtil.mul(R2, joint.getLocalAnchor2());
            p2.add(x2);

            g.setColor(Color.red);
            g.drawLine((int) x1.getX(), (int) x1.getY(), (int) p1.x, (int) p1.y);
            g.drawLine((int) p1.x, (int) p1.y, (int) p2.getX(), (int) p2.getY());
            g.drawLine((int) p2.getX(), (int) p2.getY(), (int) x2.getX(), (int) x2.getY());
        }

    }

    /**
     * Draw a specific contact point determined from the simulation
     * 
     * @param g The graphics context on which to draw
     * @param contact The contact to draw
     */
    protected void drawContact(Graphics2D g, Contact contact) {
        int x = (int) contact.getPosition().getX();
        int y = (int) contact.getPosition().getY();
        g.setColor(Color.WHITE);
        g.fillOval(x - 3, y - 3, 6, 6);
        g.setColor(BLACK);
        g.drawOval(x - 3, y - 3, 6, 6);
    }

    private class Animator implements Runnable {

        protected boolean running = false;
        protected boolean pause = false;
        protected long targetTime = 1000000 / 60; //60fps
        protected int worldUpdatesPerFrame = 10;
        protected long beforeTime,  lastFrame,  sleepTime;
        protected long overSleepTime = 0L;

        public void run() {
            running = true;
            beforeTime = System.nanoTime();
            while (running) {
                gameTick();
            }
        }

        private void gameTick() {
            //adaptive timing
            lastFrame = System.nanoTime() - beforeTime;
            sleepTime = (targetTime - lastFrame) / 1000;

            //update
            if (!pause) {
                updateWorld();
            }

            //render
            renderWorldState();

            //sleep
            try {
                Thread.sleep(sleepTime > 0 ? sleepTime : 5);
            } catch (InterruptedException ex) {
                Logger.getLogger(IncredulousPanel.class.getName()).log(Level.SEVERE, null, ex);
            }
            beforeTime = System.nanoTime();

        }

        private void renderWorldState() {
            repaint();
        }

        private void updateWorld() {
            if (reset) {
                reset = false;
                gameWorld.clear();
                initWorld(gameWorld);
            }
            for (int i = 0; i < worldUpdatesPerFrame; i++) {
                gameWorld.step();
            }
            pruneWorld(gameWorld);
        }
    }

    /**
     * I'll be back.
     * 
     * A click creates a falling ball. Random size and weight, within some limits.
     * 
     * Dragging the mouse creates a free-falling box defined by the bounding-
     * box of the shape drawn
     * 
     * Dragging the mouse and releasing the button with CTRL held makes the
     * box created fixed
     * 
     * Draggin the mouse and releasing the button with ALT held fixes the box,
     * but allows it to rotate (see-saw).
     * 
     * Clicking with SHIFT held starts drawing a polygon. Releasing the SHIFT 
     * button closes the polygon.
     * 
     */
    private class BodyBuilder extends MouseAdapter {

        private final float RESTITUTION = 0.4f;
        private final float DAMPING = 0.00001f;
        private final float ROT_DAMPING = 0.0000001f;
        boolean polygonDraw = false;
        boolean freeDraw = false;

        @Override
        public void mouseClicked(MouseEvent e) {
            Point p = e.getPoint();
            Body built = new Body("Circle",
                    new Circle(5f + (random.nextFloat() * 5f)),
                    5f + (random.nextFloat() * 50f));
            setBodyFreefalling(built);
            built.setPosition(p.x, p.y);
            addBody(built);
        }
        private long lastCaughtPointTime;
        private long currentTime;
        private long timePointCatchInterval = 50;

        @Override
        public void mouseDragged(MouseEvent e) {
            if (freeDraw) {
                currentTime = System.currentTimeMillis();

                if (lastCaughtPointTime + timePointCatchInterval < currentTime) {
                    //time to get another point
                    Point p = e.getPoint();
                    if (!drawn.contains(p)) {
                        drawn.add(p);
                    }
                    lastCaughtPointTime = currentTime;
                }
            } else {
                freeDraw = true;
                lastCaughtPointTime = System.currentTimeMillis();
                drawn.clear();
                drawn.add(e.getPoint());
            }
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            if (freeDraw) {
                try {
                    Body addMe = createPolygonBody(drawn);
                    setBodyFreefalling(addMe);
                    if (e.isAltDown()) {
                        setBodyFixedRotatable(addMe);
                    }
                    if (e.isControlDown()) {
                        setBodyFixed(addMe);
                    }
                    addBody(addMe);
                    drawn.clear();
                } catch (OutOfShapeException ex) {
                    Logger.getLogger(IncredulousPanel.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
            freeDraw = false;
        }

        private void addPolygonPoint(MouseEvent e) {
            polygonDraw = true;
            drawn.add(e.getPoint());
        }

        private Body createPolygonBody(List<Point> points)
                throws OutOfShapeException {
            Polygon shape = createPolygonShape(points);

            Body builtBody = new Body("Polygon", shape, shape.getArea() / 50);
            Rectangle bounds = getBoundsOfPoints(points);
            Point centre = new Point((int) bounds.getCenterX(),
                    (int) bounds.getCenterY());
            builtBody.setPosition(centre.x, centre.y);
            return builtBody;
        }

        private Body createBoxBody(List<Point> points)
                throws OutOfShapeException {
            if (points.size() < 1) {
                throw new OutOfShapeException(
                        "Empty list of points - cannot make box");
            }

            Rectangle bounds = getBoundsOfPoints(points);

            Box BoundingBox = new Box(bounds.width, bounds.height);
            Body BeatBox = new Body(BoundingBox, BoundingBox.getSize().length());


            Point centre = new Point((int) bounds.getCenterX(),
                    (int) bounds.getCenterY());

            BeatBox.setPosition(centre.x, centre.y);

            return BeatBox;
        }

        /**
         * Creates a polygon out of the points captured. 
         * @param points
         * @return
         */
        private Polygon createPolygonShape(List<Point> points)
                throws OutOfShapeException {
            if (points.size() < 3) {
                throw new OutOfShapeException("At least 3 points needed to create polygon");
            }
            ArrayList<ROVector2f> physPoints = new ArrayList<ROVector2f>(points.size());

            Rectangle bounds = getBoundsOfPoints(points);

            for (Point p : points) {
                ROVector2f v = new Vector2f((float) (p.x - (bounds.x + (bounds.width / 2))),
                        (float) (p.y - (bounds.y + (bounds.height / 2))));
                physPoints.add(v);
            }

            //parse the points into a shape
            ROVector2f[] vectors = new ROVector2f[physPoints.size()];
            Polygon poly = new Polygon(physPoints.toArray(vectors));

            return poly;
        }

        private void setBodyFreefalling(Body b) {
            b.setMoveable(true);
            b.setRotatable(true);
            b.setCanRest(true);
            b.setRestitution(RESTITUTION);
            b.setDamping(DAMPING);
            b.setRotDamping(ROT_DAMPING);
        }

        private void setBodyFixed(Body b) {
            b.setMoveable(false);
            b.setRotatable(false);
            b.setCanRest(true);
            b.setRestitution(RESTITUTION);
            b.setDamping(DAMPING);
            b.setRotDamping(ROT_DAMPING);
        }

        private void setBodyFixedRotatable(Body b) {
            b.setMoveable(false);
            b.setRotatable(true);
            b.setCanRest(true);
            b.setRestitution(RESTITUTION);
            b.setDamping(DAMPING);
            b.setRotDamping(ROT_DAMPING);
        }

        private Rectangle getBoundsOfPoints(List<Point> points) {
            int minX = points.get(0).x;
            int maxX = points.get(0).x;
            int minY = points.get(0).y;
            int maxY = points.get(0).y;
            for (Point p : drawn) {
                if (p.x < minX) {
                    minX = p.x;
                }
                if (p.x > maxX) {
                    maxX = p.x;
                }
                if (p.y < minY) {
                    minY = p.y;
                }
                if (p.y > maxY) {
                    maxY = p.y;
                }

            }
            int width = maxX - minX;
            int height = maxY - minY;

            return new Rectangle(minX, minY, width, height);
        }
    }

    private class KeyMaster extends KeyAdapter {

        @Override
        public void keyPressed(KeyEvent e) {
            switch (e.getKeyCode()) {
                case VK_M:
                    try {
                        if (musicPlayer.getStatus() == BasicPlayer.PLAYING) {
                            System.out.println("Music should pause");
                            musicPlayer.pause();
                        } else {

                            System.out.println("Music should play");
                            musicPlayer.resume();
                        }
                    } catch (BasicPlayerException ex) {
                        Logger.getLogger(IncredulousPanel.class.getName()).log(Level.SEVERE, null, ex);
                    }
                    break;

                case VK_R:
                    reset = true;
                    break;
                case VK_P:
                    animator.pause = !animator.pause;
                    break;
                case VK_ESCAPE:
                case VK_Q:
                case VK_END:
                    animator.running = false;
                    System.exit(0);
                default:
                    return;
            }
        }
    }
}
